/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.server;

import locusts.server.physics.PhysicsSystem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import locusts.common.Constants;
import locusts.common.ObservableList;
import locusts.common.Player;
import locusts.common.World;
import locusts.common.entities.Entity;
import locusts.common.entities.EntityData;
import locusts.common.entities.EntityDataList;
import locusts.common.entities.EntityList;
import locusts.common.entities.EntityType;
import locusts.common.entities.EntityTypeList;
import locusts.common.net.InputMessage;
import locusts.lib.FrameCounter;
import locusts.server.behaviours.BehaviourSystem;
import locusts.server.collisions.Collidable;
import locusts.server.collisions.CollidableEntityAdapter;
import locusts.server.collisions.CollisionSystem;
import org.apache.mina.common.IoSession;
import org.apache.mina.handler.demux.MessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Game class is the primary model of the game and controls all operations
 * on the model. It is composed of a number of sub components that handle
 * each of data types used in the game.
 *
 * @author Hamish Morgan
 */
public class Game {

    public static final long MAX_FPS = 1000L / 30L;
    private static final Logger LOG = LoggerFactory.getLogger(Game.class);
    //
    private final List<GameListener> listeners =
            new CopyOnWriteArrayList<GameListener>();
    //
    private final ObservableList<Player> players;
    private FrameCounter frameCounter;
    private Timer timer;
    //
    private World world;
    private final BehaviourSystem behaviourSystem;
    private final CollisionSystem collisionSystem;
    private final PhysicsSystem physicsSystem;
    private final EntityDataList entityData;
    private final InputController controller;
    private final EventManager eventManager;

    public Game(World world) {
        this.world = world;
        entityData = new EntityDataList();
        behaviourSystem = new BehaviourSystem(getEntities(), entityData);
        collisionSystem = new CollisionSystem();
        physicsSystem = new PhysicsSystem();
        controller = new InputController(this);
        eventManager = new EventManager();
        players = new ObservableList<Player>(new ArrayList<Player>());
    }

    public World getWorld() {
        return world;
    }

    public BehaviourSystem getBehaviourSystem() {
        return behaviourSystem;
    }

    public EventManager getEventManager() {
        return eventManager;
    }

    public EntityList getEntities() {
        return world.getEntities();
    }

    public EntityTypeList getEntityTypes() {
        return world.getTypes();
    }

    public PhysicsSystem getPhysicsSystem() {
        return physicsSystem;
    }

    public EntityDataList getEntityData() {
        return entityData;
    }

    public CollisionSystem getCollisionSystem() {
        return collisionSystem;
    }

    public InputController getController() {
        return controller;
    }

    public ObservableList<Player> getPlayers() {
        return players;
    }

    public FrameCounter getFrameCounter() {
        return frameCounter;
    }

    public void addEntity(Entity e) {
        getEntities().add(e);
        world.setModified();
        if (e instanceof Collidable) {
            final Collidable c = (Collidable) e;
            collisionSystem.addCollidable(c);
        } else if (getEntityData().containsKey(e.getTypeId())) {
            final Collidable c = new CollidableEntityAdapter(e, this.
                    getEntityData().get(e));
            collisionSystem.addCollidable(c);
        }
        fireGameEntitiesAddedEvent();
    }

    public void removeEntity(Entity e) {

        if (e instanceof Collidable) {
            final Collidable c = (Collidable) e;
            collisionSystem.removeCollidable(c);
        } else if (getEntityData().containsKey(e.getTypeId())) {
            final Collidable c = collisionSystem.getCollidable(e.getId());
            collisionSystem.removeCollidable(c);
        }
        getEntities().remove(e);
        getPhysicsSystem().removeJointsContaining(e);
        world.setModified();
        fireGameEntitiesRemovedEvent();
    }

    private Player getLocalInstanceOf(Player player) {
        int index = players.indexOf(player);
        if (index == -1)
            return player;
        else return players.get(index);
    }

    public class InputController {

        private final Logger USER_LOG = LoggerFactory.getLogger("User");
        private final Game game;
        private Map<Player, Entity> playerInput =
                new HashMap<Player, Entity>();

        public InputController(Game game) {
            this.game = game;
        }
        public final InputHandler inputHandler = new InputHandler();

        public class InputHandler implements MessageHandler<InputMessage> {

            public void messageReceived(IoSession session, InputMessage msg)
                    throws Exception {
                LOG.info("Recieved input message {}", msg);
                Player player = getLocalInstanceOf(msg.getPlayer());

                USER_LOG.info("{}", msg);
                switch (msg.getType()) {
                    case PRESS:
                        playerInputDown(player, msg);
                        break;
                    case RELEASE:
                        playerInputUp(player);
                        break;

                    case DRAG:
                        playerInputDrag(player, msg);
                        break;
                    default:
                        LOG.warn("Unknown message type {}", msg.getType());
                }
            }
        }

        public void doCollisions(Entity e) {
            final Collidable c = e instanceof Collidable
                    ? (Collidable) e
                    : new CollidableEntityAdapter(e, game.getEntityData().get(
                    e));
            game.getCollisionSystem().doCollisions(c, 0);
        }

        public void playerInputDown(Player player, InputMessage msg) {

            playerInputUp(player);

            final Entity press = GameFactory.makeInputDown(game, msg, player);
            doCollisions(press);

            playerInput.put(player, press);

            GameFactory.makeInputDrag(game, press);
            game.addEntity(press);
            playerInput.put(player, press);
        }

        public void playerInputUp(Player player) {
            if (playerInput.containsKey(player)) {
                final Entity release = playerInput.remove(player);
                game.removeEntity(release);
            }
        }

        public void playerInputDrag(Player player, InputMessage msg) {
            final int dragId = game.getEntityTypes().getName(
                    Constants.MOUSE_MOVE).
                    getId();

            if (playerInput.containsKey(player)) {

                final Entity drag = playerInput.get(player);

                if (drag.getTypeId() != dragId)
                    GameFactory.makeInputDrag(game, drag);

                GameFactory.updateInputEntity(drag, msg);
            }
        }
    }

    public void start() {
        LOG.info("Game started,");
        frameCounter = new FrameCounter("Server:Game");
        timer = new Timer("Server-Game-Thread");
        timer.schedule(new UpdateTimer(), 0, MAX_FPS);
    }

    public void stop() {
        LOG.info("Game stopped.");
        timer.cancel();
        timer.purge();
    }

    private class UpdateTimer extends TimerTask {

        @Override
        public void run() {
            LOG.debug("Game tick.");
            FrameCounter frameCounter = getFrameCounter();
            final double period = frameCounter.getTimeSinceLastFrameSeconds();
            frameCounter.markFrame();
            frameCounter.getFps();

//            final double energyDepletion = Math.pow(0.95, period);
            for (Entity e : getEntities()) {
                final EntityData d = getEntityData().get(e);

                if (!e.isAlive() || d == null)
                    continue;

                if (d.getEnergyDepletionRate() != 0) {
                    double energyDepletion = Math.pow(1 - d.
                            getEnergyDepletionRate(), period);
                    e.setEnergy(e.getEnergy() * energyDepletion);
                    e.setModified();
                }


                if (d.isDieOnMinEnergy() && e.getEnergy() <= d.getMinEnergy()) {
                    e.setAlive(false);
                    e.setModified();
                }
            }

            List<Entity> dead = getEntities().getDeadEntities();
            if (dead.size() > 0) {
                for (Entity e : dead) {
                    if (e.getTypeId() ==
                            getEntityTypes().getName(Constants.LOCUST).
                            getId()) {
                        addEntity(GameFactory.makeDeadLocust(Game.this, e));
                    }



                    removeEntity(e);
                }
            }

            getBehaviourSystem().update(period);

            getWorld().update(period);

            getCollisionSystem().doCollisions(period);

            getPhysicsSystem().update(period);

            // Add time to player score. Only do this every second  to
            // reduce unnessary updates.
            // Also set the crops value to reflect that of the players farm.
            if (world.getState() != World.State.GAME_OVER 
                    && frameCounter.getFrameCount() % MAX_FPS == 0)
            {
                for (Player player : players) {
                    player.addTime(1);
//                    final int farmId = player.getFarmId();
//                    final Entity farm = getEntities().getById(farmId);
//                    final double crops = farm == null ? 0 : farm.getEnergy();
//                    player.setCrops(crops);
//                    player.setModified(true);
                }
            }

            // If all the farms have minimum energy then the game is over.
            {
                boolean gameOver = true;
                final EntityType type = getEntityTypes().getName(
                        Constants.FARM);
                final EntityData data = getEntityData().get(type);
                for (Entity farm : getEntities().getAllByType(type)) {
                    if (farm.getEnergy() > data.getMinEnergy()) {
                        gameOver = false;
                        break;
                    }
                }
                if (gameOver) {
                    world.setState(World.State.GAME_OVER);
                } else {
                    world.setState(World.State.RUNNING);
                }
            }

            fireGameUpdatedEvent();
        }
    }

    public void addPlayer(Player player) {
        if (players.add(player)) {
            firePlayerJoinedEvent(player);
        }
    }

    public void removePlayer(Player player) {
        if (players.remove(player)) {
            firePlayerLeftEvent(player);
        }
    }

// ========================================================
//
    private void fireGameUpdatedEvent() {
        for (GameListener listener : listeners) {
            listener.gameUpdated(this);
        }
    }

    private void fireGameEntitiesAddedEvent() {
        for (GameListener listener : listeners) {
            listener.gameEntitiesAdded(this);
        }
    }

    private void fireGameEntitiesRemovedEvent() {
        for (GameListener listener : listeners) {
            listener.gameEntitiesRemoved(this);
        }
    }

    private void firePlayerJoinedEvent(Player p) {
        for (GameListener listener : listeners) {
            listener.playerJoined(this, p);
        }

    }

    private void firePlayerLeftEvent(Player p) {
        for (GameListener listener : listeners) {
            listener.playerLeft(this, p);
        }

    }

    public void addGameListener(GameListener listener) {
        if (listener == null)
            throw new NullPointerException();
        if (listeners.contains(listener))
            throw new IllegalArgumentException();
        listeners.add(listener);
    }

    public void removeGameListener(GameListener listener) {
        if (listener == null)
            throw new NullPointerException();
        if (!listeners.contains(listener))
            throw new IllegalArgumentException();
        listeners.remove(listener);
    }
}
